/** 
 * videoCall 1.0.0
 * powerBy 王小向
 */
class VideoCall {

    constructor({ socket, timeout, confirm, onStatusChange, onMessage, messageMap, remoteVideoEle, localVideoEle, userId }) {
        this.socket = socket;
        this.timeout = timeout
        this.confirm = confirm
        this.onStatusChange = onStatusChange
        this.onMessage = onMessage
        this.remoteVideoEle = remoteVideoEle
        this.localVideoEle = localVideoEle
        this.userId = userId
        this.pc = null;
        this.localMediaStream = null;
        this.mountSocketEvent(socket);
        this.callTimerId = null
        this.status = 'free'//free-空闲，calling-呼叫中,called-被呼叫中，通话连接-connecting,busy-通话中 
        this.messageMap = this.getMessageMap(messageMap)
        this.deviceType = this.getDeviceType()
        this.otherId = null

    }

    send(to, type, data) {
        const sendDataStr = JSON.stringify({
            type,
            from: this.userId,
            to,
            data,
            timeStamp: Date.now()
        })
        this.socket.send(sendDataStr)
    }

    sendToOther(type, data) {
        if (this.otherId == null) {
            throw new Error(this.t('noOther'))
        }
        this.send(this.otherId, type, data)
    }

    getDeviceType() {
        const uA = navigator.userAgent.toLowerCase();
        const ipad = uA.match(/ipad/i) == "ipad";
        const iphone = uA.match(/iphone os/i) == "iphone os";
        const android = uA.match(/android/i) == "android";
        const windowsce = uA.match(/windows ce/i) == "windows ce";
        const windowsmd = uA.match(/windows mobile/i) == "windows mobile";
        if (ipad || iphone || android || windowsce || windowsmd) {
            return 'phone'
        } else {
            return 'pc'
        }
    }

    getMessageMap(messageMap) {
        if (messageMap && messageMap.toString() === "[object Object]") {
            return messageMap
        } else {
            return {
                hangUp: '对方已经挂断，连接失败',
                rejectCall: '对方拒绝通话',
                busy: '对方正在通话中',
                timeout: '对方未接听',
                noVideo: '未发现摄像头',
                noAudio: '未发现麦克风',
                opendDeviceError: '打开摄像头或者麦克风失败',
                notSupportWebRTC: '浏览器不支持WebRTC！',
                connectionSuccess: '连接成功',
                connectionError: '连接失败',
                noOther: '未获取到对方的ID'
            }
        }
    }

    t(code) {
        return this.messageMap[code]
    }

    emitMessag(type, message) {
        if (typeof this.onMessage === 'function') {
            this.onMessage({ type, message })
        }
    }

    statusChange(status) {
        this.status = status
        if (typeof this.onStatusChange === 'function') {
            this.onStatusChange(this.status)
        }
    }

    readyPeerConnection() {
        const PeerConnection =
            window.PeerConnection ||
            window.webkitPeerConnection ||
            window.webkitRTCPeerConnection ||
            window.mozRTCPeerConnection;
        if (!PeerConnection) {
            throw new Error(this.t('notSupportWebRTC'));
        }
        const pc = new PeerConnection();
        this.pc = pc;
        pc.ontrack = (e) => {
            if (e && e.streams) {
                this.remoteVideoEle.srcObject = e.streams[0];
                this.statusChange('busy')
                const msg = this.t('connectionSuccess')
                this.emitMessag('success', msg)
            }
        };
        pc.oniceconnectionstatechange = () => {
            if (
                pc.iceConnectionState === 'failed' ||
                pc.iceConnectionState === 'disconnected' ||
                pc.iceConnectionState === 'closed'
            ) {
                this.closeConnection();
            }
        };

        pc.onicecandidate = (e) => {
            if (e.candidate) {
                this.sendToOther('ice_candidate', e.candidate)
            } 
        };
    }

    mountSocketEvent(socket) {
        const actionMap = {
            offer: this.hanldeRecieveOffer,
            answer: this.handleRecieveAnswer,
            ice_candidate: this.handleIceCandidate,
            request_call: this.handleRequestCall,
            allow_call: this.handleAllowCall,
            reject_call: this.handleRejected,
            close_connection: this.closeConnection,
        };

        socket.onmessage = async (e) => {
            const responsData = JSON.parse(e.data);
            const { type, data, from } = responsData
            const actionFunc = actionMap[type];
            if (typeof actionFunc !== 'function') return;
            if (this.otherId === from || type === 'request_call') {
                await actionFunc.call(this, data,from);
            }
        };
    }

    async hanldeRecieveOffer(data) {
        const offerSdp = new RTCSessionDescription(data);
        await this.pc.setRemoteDescription(offerSdp);
        const answer = await this.pc.createAnswer();
        await this.pc.setLocalDescription(answer);
        this.sendToOther('answer', answer);
    }

    async handleRecieveAnswer(data) {
        const answerSDP = new RTCSessionDescription(data);
        await this.pc.setRemoteDescription(answerSDP);
    }

    async handleIceCandidate(data) {
        await this.pc.addIceCandidate(data);
    }

    reset() {
        this.otherId = null
        this.pc = null
        this.closeVideo();
        this.statusChange('free')
    }

    closeConnection(data) {
        if (data) {
            this.emitMessag('error', data)
        }
        if (this.pc) {
            this.pc.close();
        }
        this.reset()
    }

    async handleAllowCall() {
        if (!this.pc) {
            this.sendToOther('close_connection', this.t('oppositeHangup'))
        } else {
            clearTimeout(this.callTimerId)
            this.callTimerId = null
            await this.sendOffer();
            this.statusChange('connecting')
        }

    }

    async handleRejected(data) {
        this.emitMessag('error', data)
        if(this.callTimerId){
            clearTimeout(this.callTimerId)
            this.callTimerId = null
        }
        this.closeConnection()
    }

    async confirmCall(oterhId) {
        this.statusChange('called')
        const allowCallFlag = await this.confirm();
        if (this.status !== 'called') {
            this.emitMessag('error', this.t('connectionError'))
            this.reset()
            return
        }
        if (allowCallFlag) {
            await this.agreeCall(oterhId)
        } else {
            this.disAgreeCall(oterhId)
        }
    }

    async ready(){
        this.readyPeerConnection();
        this.readVideoEle();
        await this.oppenLocalVideo();
    }

    async agreeCall(otherId) {
        try {
            this.otherId = otherId
            this.statusChange('connecting');
            await this.ready()
            this.sendToOther('allow_call');
        } catch (e) {
            this.emitMessag(e.message)
            this.reset()
        }

    }

    disAgreeCall(otherId) {
        this.send(otherId, 'reject_call', this.t('rejectCall'));
        this.statusChange('free')
    }

    async handleRequestCall(data,otherId) {
        if (this.status === 'free') {
            await this.confirmCall(otherId);
        } else {
            this.send(otherId, 'reject_call', this.t('busy'))
        }
    }

    async sendOffer() {
        const myOffer = await this.pc.createOffer();
        await this.pc.setLocalDescription(myOffer);
        this.sendToOther('offer', myOffer);

    }

    async call(otherId) {
        try {
            this.otherId = otherId
            this.statusChange('calling')
            await this.ready()
            this.sendToOther('request_call');
            this.callTimerId = setTimeout(() => {
                if (this.status === 'calling') {
                    this.closeConnection(this.t('timeout'))
                    this.sendToOther('close_connection')
                }
            }, this.timeout)
            return true
        } catch (e) {
            this.emitMessag('error', e.message)
            this.reset()
            return false
        }
    }


    // 检查硬件音频、视频输入硬件
    async checkDevices() {
        const devices = await navigator.mediaDevices.enumerateDevices();
        let [video, audio] = [false, false];
        devices.forEach((device) => {
            if (device.kind === 'audioinput') {
                audio = true;
            }
            if (device.kind === 'videoinput') {
                video = true;
            }
        });
        if (!video) {
            throw new Error(this.t('noVideo'));
        }
        if (!audio) {
            throw new Error(this.t('noAudio'));
        }
    }

    async oppenLocalVideo() {
        let stream;
        const pcVideo = true
        const phoneVideo = {
            width: { min: 1024, ideal: 1280, max: 1920 },
            height: { min: 776, ideal: 720, max: 1080 }
        }
        const constraints = {
            audio: true,
            video: this.deviceType === 'pc' ? pcVideo : phoneVideo
        };
        await this.checkDevices();
        try {
            stream = await navigator.mediaDevices.getUserMedia(
                constraints
            );
            this.localVideoEle.srcObject = stream;
        } catch {
            throw new Error('opendDeviceError');
        }
        this.localMediaStream = stream
        stream.getTracks().forEach((track) => {
            this.pc.addTrack(track, stream);
        });
    }

    readVideoEle() {
        this.localVideoEle.onloadeddata = () => {
            this.localVideoEle.play();
        };
        this.remoteVideoEle.onloadeddata = () => {
            this.remoteVideoEle.play();
        };
    }

    hangUp() {
        this.sendToOther('close_connection')
        this.closeConnection()
    }

    closeVideo() {
        if (this.localMediaStream) {
            this.localMediaStream.getTracks().forEach(track => {
                track.stop()
            })
        }
        this.localMediaStream = null
        this.localVideoEle.srcObject = null;
        this.remoteVideoEle.srcObject = null;
    }

}